import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase'
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const supabase = createServerSupabaseClient()
const bookId = params.id
const { currentFiles } = await request.json()
// Get current user session
const authHeader = request.headers.get('authorization')
let user
try {
if (authHeader) {
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', ''))
if (!authError) user = authUser
} else {
const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser()
if (!sessionError) user = sessionUser
}
} catch (e) {
// Ignore auth errors
}
if (!user) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
// Get GitHub integration
const { data: profile } = await supabase
.from('profiles')
.select('github_integrations')
.eq('id', user.id)
.single()
const integration = profile?.github_integrations?.[bookId]
if (!integration) {
return NextResponse.json(
{ error: 'GitHub integration not found' },
{ status: 404 }
)
}
const owner = integration.github_username
const repo = integration.repository_name
const accessToken = integration.access_token
try {
// Get the diff between current files and the latest commit
const diffResult = await generateGitStyleDiff(
owner,
repo,
accessToken,
currentFiles
)
return NextResponse.json({
changes: diffResult.changes,
summary: diffResult.summary,
diffText: diffResult.diffText
})
} catch (githubError) {
console.error('โ GitHub Diff API: Error generating diff:', githubError)
return NextResponse.json(
{ error: 'Failed to generate diff', details: githubError instanceof Error ? githubError.message : String(githubError) },
{ status: 500 }
)
}
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// Helper function to generate git-style diff
async function generateGitStyleDiff(
owner: string,
repo: string,
accessToken: string,
currentFiles: Array<{ name: string; content: string; path: string }>
): Promise<{
changes: Array<{
path: string
changeType: 'added' | 'modified' | 'deleted'
linesAdded: number
linesRemoved: number
currentContent: string
committedContent: string
}>
summary: {
filesChanged: number
insertions: number
deletions: number
}
diffText: string
}> {
console.log('๐ GitHub Diff API: Generating git-style diff')
// Get the latest commit SHA
const latestCommitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!latestCommitResponse.ok) {
if (latestCommitResponse.status === 409) {
// Repository is empty - all files are new
const changes = currentFiles.map(file => ({
path: file.path,
changeType: 'added' as const,
linesAdded: file.content.split('\n').length,
linesRemoved: 0,
currentContent: file.content,
committedContent: ''
}))
const summary = {
filesChanged: changes.length,
insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0),
deletions: 0
}
return { changes, summary, diffText: generateDiffText(changes) }
}
throw new Error(`Failed to get latest commit: ${latestCommitResponse.status}`)
}
const latestCommit = await latestCommitResponse.json()
// Get the tree for the latest commit
const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${latestCommit.sha}?recursive=1`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!treeResponse.ok) {
throw new Error(`Failed to get repository tree: ${treeResponse.status}`)
}
const treeData = await treeResponse.json()
const committedFiles: { [path: string]: string } = {}
// Get content for all committed files
const fileBlobs = treeData.tree.filter((item: any) =>
item.type === 'blob' &&
item.path !== 'README.md' &&
!item.path.startsWith('.git')
)
console.log(`๐ GitHub Diff API: Processing ${fileBlobs.length} committed files`)
// Fetch committed file contents in batches
const batchSize = 10
for (let i = 0; i < fileBlobs.length; i += batchSize) {
const batch = fileBlobs.slice(i, i + batchSize)
await Promise.all(batch.map(async (blob: any) => {
try {
const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (blobResponse.ok) {
const blobData = await blobResponse.json()
let content = ''
if (blobData.encoding === 'base64') {
try {
content = Buffer.from(blobData.content, 'base64').toString('utf-8')
} catch {
// Skip binary files
return
}
} else {
content = blobData.content
}
committedFiles[blob.path] = content
}
} catch (error) {
console.error(`โ GitHub Diff API: Error fetching blob ${blob.path}:`, error)
}
}))
}
// Compare current files with committed files
const changes: Array<{
path: string
changeType: 'added' | 'modified' | 'deleted'
linesAdded: number
linesRemoved: number
currentContent: string
committedContent: string
}> = []
// Check for added and modified files
currentFiles.forEach(currentFile => {
const committedContent = committedFiles[currentFile.path] || ''
if (currentFile.content !== committedContent) {
const currentLines = currentFile.content.split('\n')
const committedLines = committedContent.split('\n')
changes.push({
path: currentFile.path,
changeType: committedContent ? 'modified' : 'added',
linesAdded: Math.max(0, currentLines.length - committedLines.length),
linesRemoved: Math.max(0, committedLines.length - currentLines.length),
currentContent: currentFile.content,
committedContent
})
}
})
// Check for deleted files
Object.keys(committedFiles).forEach(committedPath => {
const currentFileExists = currentFiles.some(f => f.path === committedPath)
if (!currentFileExists) {
const committedContent = committedFiles[committedPath]
const committedLines = committedContent.split('\n')
changes.push({
path: committedPath,
changeType: 'deleted',
linesAdded: 0,
linesRemoved: committedLines.length,
currentContent: '',
committedContent
})
}
})
const summary = {
filesChanged: changes.length,
insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0),
deletions: changes.reduce((sum, change) => sum + change.linesRemoved, 0)
}
return {
changes,
summary,
diffText: generateDiffText(changes)
}
}
// Helper function to generate unified diff text
function generateDiffText(changes: Array<{
path: string
changeType: 'added' | 'modified' | 'deleted'
currentContent: string
committedContent: string
}>): string {
let diffText = ''
changes.forEach(change => {
diffText += `\n--- a/${change.path}\n+++ b/${change.path}\n`
if (change.changeType === 'added') {
const lines = change.currentContent.split('\n')
diffText += `@@ -0,0 +1,${lines.length} @@\n`
lines.forEach(line => {
diffText += `+${line}\n`
})
} else if (change.changeType === 'deleted') {
const lines = change.committedContent.split('\n')
diffText += `@@ -1,${lines.length} +0,0 @@\n`
lines.forEach(line => {
diffText += `-${line}\n`
})
} else {
// Modified file - generate a simple unified diff
const oldLines = change.committedContent.split('\n')
const newLines = change.currentContent.split('\n')
diffText += `@@ -1,${oldLines.length} +1,${newLines.length} @@\n`
// Simple line-by-line comparison (not optimal but works)
const maxLines = Math.max(oldLines.length, newLines.length)
for (let i = 0; i < maxLines; i++) {
const oldLine = oldLines[i] || ''
const newLine = newLines[i] || ''
if (oldLine !== newLine) {
if (oldLines[i] !== undefined) diffText += `-${oldLine}\n`
if (newLines[i] !== undefined) diffText += `+${newLine}\n`
} else {
diffText += ` ${oldLine}\n`
}
}
}
})
return diffText
}